"""
This script updates two files with the RBAC scope descriptions found in
`scopes.py`.

The files are:

    1. scope-table.md

       This file is git ignored and referenced by the documentation.

    2. rest-api.yml

       This file is JupyterHub's REST API schema. Both a version and the RBAC
       scopes descriptions are updated in it.
"""

from collections import defaultdict
from pathlib import Path

from pytablewriter import MarkdownTableWriter
from ruamel.yaml import YAML

HERE = Path(__file__).parent.absolute()
DOCS = HERE / ".." / ".."
REST_API_YAML = DOCS.joinpath("source", "_static", "rest-api.yml")
SCOPE_TABLE_MD = HERE.joinpath("scope-table.md")


def _load_jupyterhub_info():
    """
    The equivalent of

        from jupyterhub import __version__
        from jupyterhub.scopes import scope_definitions

    but without needing to install JupyterHub and dependencies
    so that we can run this pre-commit
    """
    root = HERE / ".." / ".." / ".."
    g = {}
    exec((root / "jupyterhub" / "_version.py").read_text(), g)

    # To avoid parsing the whole of scope_definitions.py just pull out
    # the relevant lines
    scopes_file = root / "jupyterhub" / "scopes.py"
    scopes_lines = []
    for line in scopes_file.read_text().splitlines():
        if not scopes_lines and line == "scope_definitions = {":
            scopes_lines.append(line)
        elif scopes_lines:
            scopes_lines.append(line)
            if line == "}":
                break

    exec("\n".join(scopes_lines), g)

    return g["__version__"], g["scope_definitions"]


class ScopeTableGenerator:
    def __init__(self):
        self.version, self.scopes = _load_jupyterhub_info()

    @classmethod
    def create_writer(cls, table_name, headers, values):
        writer = MarkdownTableWriter()
        writer.table_name = table_name
        writer.headers = headers
        writer.value_matrix = values
        writer.margin = 1
        return writer

    def _get_scope_relationships(self):
        """Returns a tuple of dictionary of all scope-subscope pairs and a list of just subscopes:

        ({scope: subscope}, [subscopes])

        used for creating hierarchical scope table in _parse_scopes()
        """
        pairs = []
        for scope, data in self.scopes.items():
            subscopes = data.get('subscopes')
            if subscopes is not None:
                for subscope in subscopes:
                    pairs.append((scope, subscope))
            else:
                pairs.append((scope, None))
        subscopes = [pair[1] for pair in pairs]
        pairs_dict = defaultdict(list)
        for scope, subscope in pairs:
            pairs_dict[scope].append(subscope)
        return pairs_dict, subscopes

    def _get_top_scopes(self, subscopes):
        """Returns a list of highest level scopes
        (not a subscope of any other scopes)"""
        top_scopes = []
        for scope in self.scopes.keys():
            if scope not in subscopes:
                top_scopes.append(scope)
        return top_scopes

    def _parse_scopes(self):
        """Returns a list of table rows where row:
        [indented scopename string, scope description string]"""
        scope_pairs, subscopes = self._get_scope_relationships()
        top_scopes = self._get_top_scopes(subscopes)

        table_rows = []
        md_indent = "&nbsp;&nbsp;&nbsp;"

        def _add_subscopes(table_rows, scopename, depth=0):
            description = self.scopes[scopename]['description']
            doc_description = self.scopes[scopename].get('doc_description', '')
            if doc_description:
                description = doc_description
            table_row = [f"{md_indent * depth}`{scopename}`", description]
            table_rows.append(table_row)
            for subscope in scope_pairs[scopename]:
                if subscope:
                    _add_subscopes(table_rows, subscope, depth + 1)

        for scope in top_scopes:
            _add_subscopes(table_rows, scope)

        return table_rows

    def write_table(self):
        """Generates the RBAC scopes reference documentation as a markdown table
        and writes it to the .gitignored `scope-table.md`."""
        filename = SCOPE_TABLE_MD
        table_name = ""
        headers = ["Scope", "Grants permission to:"]
        values = self._parse_scopes()
        writer = self.create_writer(table_name, headers, values)

        title = "Table 1. Available scopes and their hierarchy"
        content = f"{title}\n{writer.dumps()}"
        with open(filename, 'w') as f:
            f.write(content)
        print(f"Generated {filename}.")
        print(
            "Run 'make clean' before 'make html' to ensure the built scopes.html contains latest scope table changes."
        )

    def write_api(self):
        """Loads `rest-api.yml` and writes it back with a dynamically set
        JupyterHub version field and list of RBAC scopes descriptions from
        `scopes.py`."""
        filename = REST_API_YAML

        yaml = YAML(typ="rt")
        yaml.preserve_quotes = True
        yaml.indent(mapping=2, offset=2, sequence=4)

        scope_dict = {}
        with open(filename) as f:
            content = yaml.load(f.read())

        content["info"]["version"] = self.version
        for scope in self.scopes:
            description = self.scopes[scope]['description']
            doc_description = self.scopes[scope].get('doc_description', '')
            if doc_description:
                description = doc_description
            scope_dict[scope] = description
        content['components']['securitySchemes']['oauth2']['flows'][
            'authorizationCode'
        ]['scopes'] = scope_dict

        with open(filename, 'w') as f:
            yaml.dump(content, f)


def main():
    table_generator = ScopeTableGenerator()
    table_generator.write_table()
    table_generator.write_api()


if __name__ == "__main__":
    main()
